﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Web.Script.Services;

namespace Muel.TypeScriptFacade.Core
{
    public class TypeScriptServiceMethod
    {
        private TypeScriptServiceManager manager;
        private MethodInfo method;

        public TypeScriptServiceMethod(TypeScriptServiceManager manager, TypeScriptService parentService, MethodInfo method)
        {
            this.manager = manager;
            this.method = method;
            this.Name = method.Name;
            this.ParentService = parentService;
        }

        private bool initialised = false;
        internal void Initialise()
        {
            try
            {
                if (!initialised)
                {
                    ParameterInfo[] methodParams = method.GetParameters();
                    foreach (ParameterInfo param in methodParams)
                    {
                        parameters.Add(param.Name, manager.ObjectManager.Create(param.ParameterType));
                    }

                    returnType = manager.ObjectManager.Create(method.ReturnType);

                    // if the return type (or the return type's element type) has types inheriting 
                    // from it, or has properties which have types inheriting from them, then check
                    // the attributes again to ensure all relevant descendent types have been added
                    // as GenerateScriptType attributes
                    TypeScriptObjectFinder finder = new TypeScriptObjectFinder(manager.ObjectManager);
                    IEnumerable<TypeScriptType> relatedTypes = finder.FindRelatedConcreteTypes(returnType);
                    
                    if (relatedTypes.Any())
                    {
                        bool foundMissingAttribute = false;
                        StringBuilder messageBuilder = new StringBuilder();
                        messageBuilder.AppendLine(string.Format("Method {0}.{1} requires the following attributes to ensure concrete types are handled correctly in the service facade:", ParentService.Name, method.Name));
                        var scriptTypeAttributes = method.GetCustomAttributes<GenerateScriptTypeAttribute>();
                        foreach (TypeScriptType type in relatedTypes)
                        {
                            if (!scriptTypeAttributes.Any(a => a.Type != null && a.Type.FullName.Equals(type.ElementType.FullName)))
                            {
                                foundMissingAttribute = true;
                                messageBuilder.AppendLine(string.Format("[GenerateScriptType(typeof({0}))]", type.FullName));
                            }
                        }
                        if (foundMissingAttribute)
                        {
                            this.messages.Add(new TypeScriptFacadeMessage(TypeScriptFacadeMessageLevel.Error, TypeScriptFacadeMessageType.MissingGenerateScriptTypeAttributes, messageBuilder.ToString()));
                        }
                    }
                }
            }
            finally
            {
                initialised = true;
            }
        }

        private TypeScriptParameterCollection parameters = new TypeScriptParameterCollection();
        public TypeScriptParameterCollection Parameters
        {
            get
            {
                Initialise();
                return parameters;
            }
        }

        private TypeScriptType returnType = null;
        public TypeScriptType ReturnType
        {
            get
            {
                Initialise();
                return returnType;
            }
        }

        public TypeScriptService ParentService { get; private set; }
        public string Name { get; private set; }

        public override string ToString()
        {
            return this.Name;
        }

        #region Output to TypeScript

        /// <summary>
        /// {0}: Service Method name (e.g. "GetContent")
        /// {1}: Parameter list (e.g. "id, name")
        /// {2}: Return type
        /// {3}: Service facade namespace
        /// {4}: Service Method Name (e.g. "GetContent")
        /// {5}: The service parameter name : value list
        /// {6}: The type to instantiate, which will be null if the type is indeterminate, and will be the underlying type of an array
        /// </summary>
        private const string methodFormat = @"
    static {0}({1}callback: (returnVal: {2}) => void) {{
        {3}.Core.Service.Call(
            this.ServiceUrl + ""/{4}"",
            {{
{5}
            }},
            callback,
{6}
        );
    }}
";
        internal string ToTypeScript()
        {
            // work out what the fallback callback parameter type is
            string fallbackBaseType = "null";
            TypeScriptType underlyingReturnType = ReturnType.GetNonCollectionElementType();
            if (!underlyingReturnType.IsPrimitiveBased && !underlyingReturnType.ChildTypes.Any()) {
                fallbackBaseType = underlyingReturnType.FullName;
            }

            string output = string.Format(methodFormat,
                Name,
                Parameters.ToTypeScriptParameterList(),
                ReturnType.GetTypeScriptType(),
                manager.Settings.ServiceFacadeName,
                Name,
                Parameters.ToTypeScriptObject(),
                fallbackBaseType
                );

            return output;
        }

        #endregion

        private List<TypeScriptFacadeMessage> messages = new List<TypeScriptFacadeMessage>();
        public TypeScriptFacadeMessage[] Messages
        {
            get
            {
                Initialise();
                return messages.ToArray();
            }
        }
    }
}
